package com.example.jpa.crud.search;

import com.example.jpa.crud.exception.ParameterMissingException;
import com.example.jpa.crud.exception.ParameterTypeConvertException;
import com.example.jpa.crud.exception.UnknownFieldException;
import com.example.jpa.crud.search.expression.ASTNode;
import com.example.jpa.crud.search.expression.ASTNodeType;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

public class SearchCondition {

    private String sql;

    private Map<String, Object> params;

    public SearchCondition() {
    }

    public SearchCondition(String sql, Map<String, Object> params) {
        this.sql = sql;
        this.params = params;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }

    public Map<String, Object> getParams() {
        return params;
    }

    public void setParams(Map<String, Object> params) {
        this.params = params;
    }

    private static final SearchCondition EMPTY = new SearchCondition("", Collections.emptyMap());

    private static final char[] chars = new char[]{
            'x', 'y', 'z', 'u', 'v', 'w',
            'a', 'b', 'c', 'd', 'e', 'f', 'g',
            'h', 'i', 'j', 'k', 'l', 'm', 'n',
            'o', 'p', 'q', 'r', 's', 't',
            'X', 'Y', 'Z', 'U', 'V', 'W',
            'A', 'B', 'C', 'D', 'E', 'F', 'G',
            'H', 'I', 'J', 'K', 'L', 'M', 'N',
            'O', 'P', 'Q', 'R', 'S', 'T'
    };

    private static String createPlaceholder(String field, int i) {
        if (i == 0) {
            return field + "_" + chars[0];
        }
        final int length = chars.length;
        final StringBuilder builder = new StringBuilder();
        while (i > 0) {
            builder.append(chars[i % length]);
            i = i / length;
        }
        return field + "_" + builder.reverse();
    }

    @SuppressWarnings("all")
    private static Object parseValue(String s, Field field) {
        if (s == null) {
            return null;
        }
        if (field == null) {
            return s;
        }
        final Class<?> dataType = field.getType();
        if (String.class.equals(dataType)) {
            return s;
        } else if (Integer.class.equals(dataType) || int.class.equals(dataType)) {
            try {
                return Integer.valueOf(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid integer32 value");
            }
        } else if (Long.class.equals(dataType) || long.class.equals(dataType)) {
            try {
                return Long.valueOf(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid integer64 value");
            }
        } else if (Boolean.class.equals(dataType) || boolean.class.equals(dataType)) {
            final String sToLowerCase = s.toLowerCase(Locale.getDefault());
            if ("true".equals(sToLowerCase)) {
                return true;
            } else if ("false".equals(sToLowerCase)) {
                return false;
            } else {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid boolean value");
            }
        } else if (Float.class.equals(dataType) || float.class.equals(dataType)) {
            try {
                return Float.valueOf(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid float32 value");
            }
        } else if (Double.class.equals(dataType) || double.class.equals(dataType)) {
            try {
                return Double.valueOf(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid float64 value");
            }
        } else if (Byte.class.equals(dataType) || byte.class.equals(dataType)) {
            try {
                return Byte.valueOf(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid integer8 value");
            }
        } else if (Short.class.equals(dataType) || short.class.equals(dataType)) {
            try {
                return Short.valueOf(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid integer16 value");
            }
        } else if (Character.class.equals(dataType) || char.class.equals(dataType)) {
            final char[] charArray = s.toCharArray();
            if (charArray.length > 0) {
                return charArray[0];
            } else {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid char value");
            }
        } else if (BigDecimal.class.equals(dataType)) {
            try {
                return new BigDecimal(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid BigDecimal value");
            }
        } else if (BigInteger.class.equals(dataType)) {
            try {
                return new BigInteger(s);
            } catch (Exception e) {
                throw new ParameterTypeConvertException("'" + s + "' is not a valid BigInteger value");
            }
        } else {
            return s;
        }
    }

    private static void resolve(
            ASTNode node,
            StringBuilder builder,
            Map<String, Object> placeholderToValue,
            Map<String, String> fieldToColumn,
            Map<String, List<String>> parameterToValues,
            Map<String, Field> fields
    ) {
        final ASTNodeType nodeType = node.getType();
        if (nodeType != ASTNodeType.BOOLEAN_EXPRESSION && nodeType != ASTNodeType.BINARY_EXPRESSION) {
            // TODO
            return;
        }
        final Operator operator = node.getOperator();
        if (operator == Operator.EQ) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` = :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                placeholderToValue.put(placeholder, parseValue(values.get(values.size() - 1), fields.get(fieldName)));
            }
        } else if (operator == Operator.NE) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` != :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                placeholderToValue.put(placeholder, parseValue(values.get(values.size() - 1), fields.get(fieldName)));
            }
        } else if (operator == Operator.LT) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` < :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                placeholderToValue.put(placeholder, parseValue(values.get(values.size() - 1), fields.get(fieldName)));
            }
        } else if (operator == Operator.GT) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` > :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                placeholderToValue.put(placeholder, parseValue(values.get(values.size() - 1), fields.get(fieldName)));
            }
        } else if (operator == Operator.LE) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` <= :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                placeholderToValue.put(placeholder, parseValue(values.get(values.size() - 1), fields.get(fieldName)));
            }
        } else if (operator == Operator.GE) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` >= :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                placeholderToValue.put(placeholder, parseValue(values.get(values.size() - 1), fields.get(fieldName)));
            }
        } else if (operator == Operator.IN) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` in (:")
                        .append(placeholder)
                        .append(") )");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null) {
                    throw new ParameterMissingException(parameterName);
                }
                final Field fieldData = fields.get(fieldName);
                final List<Object> objectList = new ArrayList<>(values.size());
                for (String value : values) {
                    objectList.add(parseValue(value, fieldData));
                }
                placeholderToValue.put(placeholder, objectList);
            }
        } else if (operator == Operator.NOT_IN) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` not in (:")
                        .append(placeholder)
                        .append(") )");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null) {
                    throw new ParameterMissingException(parameterName);
                }
                final Field fieldData = fields.get(fieldName);
                final List<Object> objectList = new ArrayList<>(values.size());
                for (String value : values) {
                    objectList.add(parseValue(value, fieldData));
                }
                placeholderToValue.put(placeholder, objectList);
            }
        } else if (operator == Operator.LIKE) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` like :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                String value = values.get(values.size() - 1).replace("\\", "\\\\");
                value = value.replace("%", "\\%");
                value = value.replace("_", "\\_");
                placeholderToValue.put(placeholder, "%" + value + "%");
            }
        } else if (operator == Operator.START_WITH) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` like :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                String value = values.get(values.size() - 1).replace("\\", "\\\\");
                value = value.replace("%", "\\%");
                value = value.replace("_", "\\_");
                placeholderToValue.put(placeholder, value + "%");
            }
        } else if (operator == Operator.END_WITH) {
            final String fieldName = node.getLeft().getValue();
            final String placeholder = createPlaceholder(fieldName, placeholderToValue.size());
            final String columnName = fieldToColumn.get(fieldName);
            if (columnName == null) {
                throw new UnknownFieldException(fieldName);
            } else {
                builder.append("(`")
                        .append(columnName)
                        .append("` like :")
                        .append(placeholder)
                        .append(")");
                final String parameterName = node.getRight().getValue();
                final List<String> values = parameterToValues.get(parameterName);
                if (values == null || values.isEmpty()) {
                    throw new ParameterMissingException(parameterName);
                }
                String value = values.get(values.size() - 1).replace("\\", "\\\\");
                value = value.replace("%", "\\%");
                value = value.replace("_", "\\_");
                placeholderToValue.put(placeholder, "%" + value);
            }
        } else if (operator == Operator.AND) {
            final List<ASTNode> conditions = new ArrayList<>(2);
            conditions.add(node.getLeft());
            conditions.add(node.getRight());
            builder.append("(");
            final Iterator<ASTNode> iterator = conditions.iterator();
            final ASTNode first = iterator.next();
            resolve(first, builder, placeholderToValue, fieldToColumn, parameterToValues, fields);
            while (iterator.hasNext()) {
                builder.append(" and ");
                resolve(iterator.next(), builder, placeholderToValue, fieldToColumn, parameterToValues, fields);
            }
            builder.append(")");
        } else if (operator == Operator.OR) {
            final List<ASTNode> conditions = new ArrayList<>(2);
            conditions.add(node.getLeft());
            conditions.add(node.getRight());
            builder.append("(");
            final Iterator<ASTNode> iterator = conditions.iterator();
            final ASTNode first = iterator.next();
            resolve(first, builder, placeholderToValue, fieldToColumn, parameterToValues, fields);
            while (iterator.hasNext()) {
                builder.append(" or ");
                resolve(iterator.next(), builder, placeholderToValue, fieldToColumn, parameterToValues, fields);
            }
            builder.append(")");
        }
    }

    public static SearchCondition of(ASTNode node, Map<String, String> fieldToColumn, Map<String, List<String>> parameterToValues, Map<String, Field> fields) throws UnknownFieldException {
        if (node == null) {
            return EMPTY;
        }
        final StringBuilder sql = new StringBuilder();
        final Map<String, Object> placeholderToValue = new HashMap<>();
        resolve(node, sql, placeholderToValue, fieldToColumn, parameterToValues, fields);
        return new SearchCondition("where " + sql + " ", placeholderToValue);
    }

}
